<?php

class HTTPRequestException extends Exception {}
class HTTPRequestRuntimeException extends HTTPRequestException {}

/**
 * Библиотека функций выполнения HTTP-запросов.
 * @package core2
 * @subpackage helpers
 */
class HTTPRequestHelper
{
    /**
     * Выполняет GET-запрос
     * В случае ошибки выбрасывается исключение
     *
     * @param string $URL URL-адрес.
     * @param assoc/string $data Данные передаваемые ресурсу (либо хеш, либо URL-кодированные данные).
     * @param int $timeout Таймаут в секундах. 0 — неограничено.
     * @return string Ответ
     */
    function GET($URL, $data = array(), $timeout = 0)
    {
        return self::request(self::normalizeURL($URL, $data), 'GET', '', $timeout);
    }

    function GET_RAW($URL, $data = array(), $timeout = 0, $add_headers = array())
    {
        return self::request(self::normalizeURL($URL, $data), 'GET', '', $timeout, $add_headers, 1);
    }

    /**
     * Выполняет POST-запрос типа application/x-www-form-urlencoded
     * В случае ошибки выбрасывается исключение
     *
     * @param string $URL URL-адрес.
     * @param assoc/string $data Данные передаваемые ресурсу (либо хеш, либо URL-кодированные данные).
     * @param int $timeout Таймаут в секундах. 0 — неограничено.
     * @return string Ответ
     */
    function POST($URL, $data = '', $timeout = 0)
    {
        return self::request(self::normalizeURL($URL), 'POST', http_build_query(self::normalizeData($data), '', '&'), $timeout);
    }

    function POST_RAW($URL, $data = '', $timeout = 0, $add_headers = array())
    {
        return self::request(self::normalizeURL($URL), 'POST', http_build_query(self::normalizeData($data), '', '&'), $timeout, $add_headers, 1);
    }

    /**
     * Выполняет POST-запрос с содержимым указанного типа.
     *
     * @param string $URL URL-адрес.
     * @param string $data Данные передаваемые ресурсу.
     * @param string $type MIME-тип.
     * @param int $timeout Таймаут в секундах. 0 — неограничено.
     * @return string Ответ
     */
    function POSTContent($URL, $data = '', $type = 'application/octet-stream', $timeout = 0)
    {
        if (! is_string($type)) {
            throw new HTTPRequestException('Bag MIME-type.');
        }
        if (! ctype_print($type)) {
            throw new HTTPRequestException('Invalid MIME-type ('.$type.').');
        }
        return self::request(self::normalizeURL($URL), 'POST', $data, $timeout, array('content-type' => $type));
    }

    function error_handler($errno, $errstr, $errfile, $errline, $errcontext)
    {
        if (isset($errcontext['curl'])) {
            @curl_close($errcontext['curl']);
        }
        restore_error_handler();
        if (isset($errcontext['before_error_lavel'])) {
            error_reporting($errcontext['before_error_lavel']);
        }
        @ob_end_clean();
        throw new HTTPRequestException($errstr);
    }

    private function normalizeURL($URL, $addition_data = array())
    {
        if (! is_string($URL)) {
            throw new HTTPRequestException('Bag argument.');
        }
        if (! ctype_graph($URL)) {
            throw new HTTPRequestException('Invalid URL ('.$URL.').');
        }
        $parsed_url = parse_url($URL);
        if (! in_array(strtolower($parsed_url['scheme']), array('http', 'https'))) {
            throw new HTTPRequestException('Only http protocol is allowed.');
        }
        if (empty($parsed_url['host']) || empty($parsed_url['path'])) {
            throw new HTTPRequestException('Invalid URL ('.$URL.').');
        }
        $new_url = strtolower($parsed_url["scheme"])."://";
        if (! empty($parsed_url["user"])) {
            $new_url .= urlencode(urldecode($parsed_url["user"]));
            if (! empty($parsed_url["pass"])) {
                $new_url .= ":".urlencode(urldecode($parsed_url["pass"]));
            }
            $new_url .= '@';
        }
        $new_url .= urlencode(urldecode($parsed_url["host"]));
        if (! empty($parsed_url["port"])) {
            $parsed_url["port"] = urldecode($parsed_url["port"]);
            if (! ctype_digit($parsed_url["port"])) {
                throw new HTTPRequestException('Invalid URL ('.$URL.').');
            }
            $parsed_url["port"] = intval($parsed_url["port"]);
            if ($parsed_url["port"] > 65535) {
                throw new HTTPRequestException('Invalid URL ('.$URL.').');
            }
            $parsed_url["port"] = strval($parsed_url["port"]);
            $new_url .= ":".$parsed_url["port"];
        }
        $new_url .= $parsed_url["path"];

        $args = array();
        if (! empty($parsed_url["query"])) {
            $args = self::normalizeData($parsed_url["query"]);
        }
        if (! empty($addition_data)) {
            $args = array_merge_recursive($args, self::normalizeData($addition_data));
        }
        if (! empty($args)) {
            $new_url .= "?".http_build_query($args, '', '&');
        }
        if (! empty($parsed_url["fragment"])) {
            $new_url .= "#".urlencode(urldecode($parsed_url["fragment"]));
        }

        return $new_url;
    }

    private function normalizeData($data)
    {
        if (is_string($data)) {
            parse_str($data, $data);
        } elseif (is_array($data)) {
            foreach ($data as $k => $v) {
                if (is_int($k)) {
                    throw new HTTPRequestException('Data can’t include numeric index.');
                }
            }
        } else {
            throw new HTTPRequestException('Bag argument.');
        }

        return $data;
    }

    private function request($URL, $method, $data, $timeout = 0, $add_headers = array(), $RAW = 0)
    {
        /**
         * @todo Сделать проверку валидности URL.
         */
        if (! is_string($method) || ! is_numeric($timeout)) {
            throw new HTTPRequestException('Bag argument.');
        }

        $method = strtoupper($method);
        if (! in_array($method, array('GET', 'POST'))) {
            throw new HTTPRequestException('May be only GET, POST methods.');
        }

        $timeout = intval($timeout);
        if ($timeout < 0) {
            throw new HTTPRequestException('Timeout invalid (mast be greater 1).');
        }

        /**
         * @todo Разобраться с ограничением времени выполнения скрипта в зависимости от таймаута.
         */

        $before_error_lavel = error_reporting(E_ALL);

        $before_error_handler = set_error_handler(array(__CLASS__, 'error_handler'));

        $response = "";
        ob_start();

            $curl = curl_init();

            if (! curl_setopt($curl, CURLOPT_URL, $URL)) {
                $error_str = '('.curl_errno($curl).'): '.curl_error($curl);
                @curl_close($curl);
                restore_error_handler();
                if (! is_null($before_error_lavel)) {
                    error_reporting($before_error_lavel);
                }
                @ob_end_clean();
                throw new HTTPRequestException('Can’t set URL ('.$URL.') '.$error_str);
            }
            $tmp_add_headers = array();
            foreach ($add_headers as $header => $value) {
                $tmp_add_headers[strtolower($header)] = $header.": ".strval($value);
            }
            if ($method == 'POST') {
                $tmp_add_headers['content-length'] = "Content-Length: ".strval(strlen($data));
            }
            $add_headers = array_values($tmp_add_headers);

            if ($method == 'POST') {
                if (! curl_setopt($curl, CURLOPT_POST, 1)) {
                    $error_str = '('.curl_errno($curl).'): '.curl_error($curl);
                    @curl_close($curl);
                    restore_error_handler();
                    if (! is_null($before_error_lavel)) {
                        error_reporting($before_error_lavel);
                    }
                    @ob_end_clean();
                    throw new HTTPRequestException('Can’t set method to POST '.$error_str);
                }
                if (! curl_setopt($curl, CURLOPT_POSTFIELDS, $data)) {
                    $error_str = '('.curl_errno($curl).'): '.curl_error($curl);
                    @curl_close($curl);
                    restore_error_handler();
                    if (! is_null($before_error_lavel)) {
                        error_reporting($before_error_lavel);
                    }
                    @ob_end_clean();
                    throw new HTTPRequestException('Can’t set POST-data '.$error_str);
                }
            }

            if ($timeout) {
                if (! curl_setopt($curl, CURLOPT_TIMEOUT, $timeout)) {
                    $error_str = '('.curl_errno($curl).'): '.curl_error($curl);
                    @curl_close($curl);
                    restore_error_handler();
                    if (! is_null($before_error_lavel)) {
                        error_reporting($before_error_lavel);
                    }
                    @ob_end_clean();
                    throw new HTTPRequestException('Can’t set timeout ('.$timeout.') '.$error_str);
                }
            }

            if (
                ! curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1)
                || ! curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0)
                || ! curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0)
                || ! curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method)
                || ! curl_setopt($curl, CURLOPT_HEADER, $RAW)
            ) {
                $error_str = '('.curl_errno($curl).'): '.curl_error($curl);
                @curl_close($curl);
                restore_error_handler();
                if (! is_null($before_error_lavel)) {
                    error_reporting($before_error_lavel);
                }
                @ob_end_clean();
                throw new HTTPRequestException('Error in tuning curl '.$error_str);
            }
            echo "\n", implode("\n", $add_headers);
            if (! curl_setopt($curl, CURLOPT_HTTPHEADER, $add_headers)) {
                $error_str = '('.curl_errno($curl).'): '.curl_error($curl);
                @curl_close($curl);
                restore_error_handler();
                if (! is_null($before_error_lavel)) {
                    error_reporting($before_error_lavel);
                }
                @ob_end_clean();
                throw new HTTPRequestException('Error in tuning curl '.$error_str);
            }

        $curl_msg = ob_get_clean();

        if (is_string($curl_msg) && strlen($curl_msg)) {

            restore_error_handler();
            if (! is_null($before_error_lavel)) {
                error_reporting($before_error_lavel);
            }
            throw new HTTPRequestException('Errors in curl working ('.$curl_msg.').');
        }
        ob_start();

            $response = curl_exec($curl);

            if (! is_string($response)) {
                $error_str = '('.curl_errno($curl).'): '.curl_error($curl);
                @curl_close($curl);
                restore_error_handler();
                if (! is_null($before_error_lavel)) {
                    error_reporting($before_error_lavel);
                }
                @ob_end_clean();

                throw new HTTPRequestRuntimeException('Errors in curl working '.$error_str."\n\n({$response})");
            }

            $response_status = intval(@curl_getinfo($curl, CURLINFO_HTTP_CODE));
            @curl_close($curl);

        $curl_msg = ob_get_clean();

        if (is_string($curl_msg) && strlen($curl_msg)) {

            restore_error_handler();
            if (! is_null($before_error_lavel)) {
                error_reporting($before_error_lavel);
            }
            throw new HTTPRequestRuntimeException('Errors in curl working ('.$curl_msg.').'."\n\n({$response})");
        }

        if (! $RAW) {
            if ($response_status !== 200) {
                restore_error_handler();
                if (! is_null($before_error_lavel)) {
                    error_reporting($before_error_lavel);
                }
                throw new HTTPRequestRuntimeException("Bad response status ({$response_status})");
            }
        }

        restore_error_handler();
        if (! is_null($before_error_lavel)) {
            error_reporting($before_error_lavel);
        }
        return $response;
    }
}

?>
